最近在写类似校园墙的小程序,记录下开发过程。
万丈高楼平地起,在实现一系列的功能之前先要做的就是“登录注册”;因为是小程序注册这块就省略了,只需要写登录的逻辑就好
前端选用了uni-app,一套代码多端发布很吸引人;后端框架选择了SpringBoot,由于是要落地玩的项目,自然是怎么效率高怎么来了

登录模块所需的依赖引入: jwt-token,spring-security

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.7.1</version>
</dependency

小程序的登录流程与一般应用没什么区别,只是多存了一个openid而已;登录流程大概如下

1: 前端调用微信的登录API,获取临时code,携带code向后端发送请求
2: 后端接收到请求,携带本次的code,appid(小程序id),secret向微信服务器发送请求,获取唯一标识openid
3: 查找数据库,未发现该openid的条目则创建(实际上就是省略的注册),插入库
4: 签发jwt-token,返回给前端;前端缓存,之后的业务请求携带token进行验证权限

做一些准备:

application.properties中增加配置项

1
2
3
小程序id
wx.appid=wx683fdea76c3825
wx.secret=0554defd893f3eec9bf4c43dd97

注意secret不会被记录,需妥善保存

JWT工具类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package com.example.wswbackend.config;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;

@Component
public class JwtUtil {
public static final long JWT_TTL = 60 * 60 * 1000L * 24 * 14; // 有效期14天
public static final String JWT_KEY = "SDFGjhdsfalshdfHFdsjkdsffHFdsjkdsfds121232131afasdfac232131afasdfac";

public static String getUUID() {
return UUID.randomUUID().toString().replaceAll("-", "");
}

//创建token
public static String createJWT(String subject) {
JwtBuilder builder = getJwtBuilder(subject, null, getUUID());
return builder.compact();
}

private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
SecretKey secretKey = generalKey();
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
if (ttlMillis == null) {
ttlMillis = JwtUtil.JWT_TTL;
}

long expMillis = nowMillis + ttlMillis;
Date expDate = new Date(expMillis);
return Jwts.builder()
.setId(uuid)
.setSubject(subject)
.setIssuer("sg")
.setIssuedAt(now)
.signWith(signatureAlgorithm, secretKey)
.setExpiration(expDate);
}

public static SecretKey generalKey() {
byte[] encodeKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
return new SecretKeySpec(encodeKey, 0, encodeKey.length, "HmacSHA256");
}

public static Claims parseJWT(String jwt) throws Exception {
SecretKey secretKey = generalKey();
return Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(jwt)
.getBody();
}
}

创建用户,权限表

user表:

1
2
3
4
5
6
7
8
9
10
11
12
-- auto-generated definition
create table user
(
openid varchar(125) null,
wx varchar(24) null,
id int auto_increment
primary key,
role int null,
password varchar(300) null,
constraint id
unique (id)
);

user_role表

1
2
3
4
5
6
7
8
9
-- auto-generated definition
create table user_role
(
id int auto_increment
primary key,
role_name varchar(100) null,
constraint id
unique (id)
);

实体类:
(使用了lombook,省略了一大堆的getset和构造器代码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.example.wswbackend.pojo;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private String openid;
private String wx;
@TableId(type = IdType.AUTO)
private Integer id;
private Integer role;
private String password;
}

配置Security

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}


@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/user/account/login/", "/user/account/register/").permitAll()
.antMatchers(HttpMethod.OPTIONS).permitAll()
.anyRequest().authenticated();

http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private UserMapper userMapper;

@Override
protected void doFilterInternal(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader("Authorization");

if (!StringUtils.hasText(token) || !token.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}

token = token.substring(7);

String userid;
try {
Claims claims = JwtUtil.parseJWT(token);
userid = claims.getSubject();
} catch (Exception e) {
throw new RuntimeException(e);
}

User user = userMapper.selectById(Integer.parseInt(userid));

if (user == null) {
throw new RuntimeException("用户名未登录");
}

UserDetailsImpl loginUser = new UserDetailsImpl(user);
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser, null, null);

SecurityContextHolder.getContext().setAuthentication(authenticationToken);

filterChain.doFilter(request, response);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserDetailsImpl implements UserDetails {

private User user;

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}

@Override
public String getPassword() {
return user.getPassword();
}

@Override
public String getUsername() {
return user.getOpenid();
}

@Override
public boolean isAccountNonExpired() {
return true;
}

@Override
public boolean isAccountNonLocked() {
return true;
}

@Override
public boolean isCredentialsNonExpired() {
return true;
}

@Override
public boolean isEnabled() {
return true;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

@Autowired
private UserMapper userMapper;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("openid", username);
User user = userMapper.selectOne(queryWrapper);
if (user == null) {
throw new RuntimeException("用户不存在");
}
return new UserDetailsImpl(user);
}
}

最后写一个接口组合起来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@Service
public class LoginServiceImpl implements LoginService {

@Autowired
private Environment environment;

@Autowired
private AuthenticationManager authenticationManager;

@Autowired
private UserMapper userMapper;

@Autowired
private PasswordEncoder passwordEncoder;

@Override
public Map<String, String> login(String code,String wx) {
String appid = environment.getProperty("wx.appid");
String secret = environment.getProperty("wx.secret");

Map<String,String> map = new HashMap<>();
String openid = GetOpenIdUtil.getopenid(appid,secret,code);
JSONObject jsonObject = JSON.parseObject(openid);
openid = jsonObject.getString("openid");
QueryWrapper<User> queryWrapper = new QueryWrapper();
queryWrapper = queryWrapper.eq("openid", openid);

List<User> userList = userMapper.selectList(queryWrapper);

if(userList.isEmpty()){
User user = new User();
user.setRole(0);
user.setWx(wx);
user.setOpenid(openid);
user.setPassword(passwordEncoder.encode(openid));
userMapper.insert(user);
}

UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(openid,openid);
Authentication authenticate = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
UserDetailsImpl userDetails = (UserDetailsImpl)authenticate.getPrincipal();
User user = userDetails.getUser();
String jwt = JwtUtil.createJWT(user.getId().toString());
map.put("error_message","success");
map.put("token",jwt);
return map;
}

获取openid的工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class GetOpenIdUtil {
public static String getopenid(String appid,String secret,String code) {
BufferedReader in = null;
String url="https://api.weixin.qq.com/sns/jscode2session?appid="
+appid+"&secret="+secret+"&js_code="+code+"&grant_type=authorization_code";
try{
URL weChatUrl = new URL(url);
URLConnection connection = weChatUrl.openConnection();
connection.setConnectTimeout(5000);
connection.setReadTimeout(5000);
connection.connect();
in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
StringBuffer sb = new StringBuffer();
String line;
while ((line = in.readLine()) != null) {
sb.append(line);
}
return sb.toString();
}catch (Exception e) {
throw new RuntimeException(e);
}
finally {
try {
if (in != null) {
in.close();
}
} catch (Exception e2) {
e2.printStackTrace();
}
}
}
}

前端部分代码太丑了不做展示,最后实现效果如下图

login 0.gif

头像的更新没录到hh